/*
 * validate.js 1.2.2
 * Copyright (c) 2011 Rick Harrison, http://rickharrison.me
 * validate.js is open sourced under the MIT license.
 * Portions of validate.js are inspired by CodeIgniter.
 * http://rickharrison.github.com/validate.js
 */

(function(window, document, undefined) {
	/*
	 * If you would like an application-wide config, change these defaults.
	 * Otherwise, use the setMessage() function to configure form specific messages.
	 */

	var defaults = {
		english : {
			required : 'The %s field is required.',
			matches : 'The %s field does not match the %s field.',
			"default" : 'The %s field is still set to default, please change.',
			valid_email : 'The %s field must contain a valid email address.',
			valid_emails : 'The %s field must contain all valid email addresses.',
			min_length : 'The %s field must be at least %s characters in length.',
			max_length : 'The %s field must not exceed %s characters in length.',
			exact_length : 'The %s field must be exactly %s characters in length.',
			greater_than : 'The %s field must contain a number greater than %s.',
			less_than : 'The %s field must contain a number less than %s.',
			alpha : 'The %s field must only contain alphabetical characters.',
			alpha_numeric : 'The %s field must only contain alpha-numeric characters.',
			alpha_dash : 'The %s field must only contain alpha-numeric characters, underscores, and dashes.',
			numeric : 'The %s field must contain only numbers.',
			integer : 'The %s field must contain an integer.',
			decimal : 'The %s field must contain a decimal number.',
			is_natural : 'The %s field must contain only positive numbers.',
			is_natural_no_zero : 'The %s field must contain a number greater than zero.',
			valid_ip : 'The %s field must contain a valid IP.',
			valid_base64 : 'The %s field must contain a base64 string.',
			valid_credit_card : 'The %s field must contain a valid credit card number.',
			is_file_type : 'The %s field must contain only %s files.',
			valid_url : 'The %s field must contain a valid URL.'
		},
		chinese_zh : {
			required : '必須填滿 %s 欄位。',
			matches : '你的 %s 欄位與 %s 欄位不符。',
			valid_email : '請填上有效的 %s ',
			valid_emails : '請填上有效的 %s ',
			min_length : '你的 %s 必須至少填滿 %s 個字元。',
			max_length : '你的 %s 必須不能超過 %s 個字元。',
			exact_length : '你的 %s 必須剛好要 %s 個字元。',
			greater_than : '你的 %s 必須要有個大於 %s 的數字。',
			less_than : '你的 %s 必須要有個小於 %s 的數字。',
			alpha : '你的 %s 必須只有英文字母。',
			alpha_numeric : '你的 %s 必須只有英文字母或數字。',
			alpha_dash : '你的 %s 只可以是英文字母、數字、底線或破折號。',
			numeric : '你的 %s 必須只有數字。',
			integer : '你的 %s 必須是個整數。',
			decimal : '你的 %s 必須是個小數點。',
			is_natural : '你的 %s 必須只有正數。',
			is_natural_no_zero : '你的 %s 必須是個大於零的數字。',
			valid_ip : '你的 %s 必須是個有效的 IP。',
			valid_base64 : '你的 %s 必須是個 base64 字串。',
			valid_credit_card : '你的 %s 必須是個有效的信用卡號碼。',
			is_file_type : '你的 %s 欄位必須要一個 %s 的檔案。',
			valid_url : '你的 %s 必須是要一個有效的 URL。'
		},
		chinese : {
			required : '%s不能为空!',
			matches : ' %s与%s输入不一致。',
			valid_email : '%s输入无效',
			valid_emails : '%s 输入无效',
			min_length : '%s至少%s个字符。',
			max_length : '%s最多%s个字符。',
			exact_length : '%s 只能 %s 个字符。',
			greater_than : '%s 必须是大于 %s 的数字。',
			less_than : '%s 必须是小于 %s 的数字。',
			alpha : '%s 必须是英文字母。',
			alpha_numeric : '你的%s必须只有字母、数字。',
			alpha_dash : '%s只能是英文字母、数字、下划线和横线',
			numeric : '%s 只能是数字。',
			integer : '%s 只能是整数。',
			decimal : '%s 必须是小数。',
			is_natural : '%s 必须是正数。',
			is_natural_no_zero : '%s 必须是大于零的数字。',
			valid_ip : '%s 必须是有效的IP。',
			valid_base64 : '%s 必须是 base64 字符。',
			valid_credit_card : '%s 必须是有效的信用卡号。',
			is_file_type : '%s 只能是 %s 的文件。',
			valid_url : '%s 必须是有效的 URL。'
		},
		callback : function(errors) {

		}
	};

	/*
	 * Define the regular expressions that will be used
	 */

	var ruleRegex = /^(.+?)\[(.+)\]$/, numericRegex = /^[0-9]+$/, integerRegex = /^\-?[0-9]+$/, decimalRegex = /^\-?[0-9]*\.?[0-9]+$/, emailRegex = /^[a-zA-Z0-9.!#$%&amp;'*+\-\/=?\^_`{|}~\-]+@[a-zA-Z0-9\-]+(?:\.[a-zA-Z0-9\-]+)*$/, alphaRegex = /^[a-z]+$/i, alphaNumericRegex = /^[a-z0-9]+$/i, alphaDashRegex = /^[a-z0-9_\-]+$/i, naturalRegex = /^[0-9]+$/i, naturalNoZeroRegex = /^[1-9][0-9]*$/i, ipRegex = /^((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[0-9]{1,2})\.){3}(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[0-9]{1,2})$/i, base64Regex = /[^a-zA-Z0-9\/\+=]/i, numericDashRegex = /^[\d\-\s]+$/, urlRegex = /^((http|https):\/\/(\w+:{0,1}\w*@)?(\S+)|)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/;

	/*
	 * The exposed public object to validate a form:
	 *
	 * @param formNameOrNode - String - The name attribute of the form (i.e. <form name="myForm"></form>) or node of the form element
	 * @param fields - Array - [{
	 *     name: The name of the element (i.e. <input name="myField" />)
	 *     display: 'Field Name'
	 *     rules: required|matches[password_confirm]
	 * }]
	 * @param callback - Function - The callback after validation has been performed.
	 *     @argument errors - An array of validation errors
	 *     @argument event - The javascript event
	 */

	var FormValidator = function(formNameOrNode, fields, language, callback) {

		this.callback = callback || defaults.callback;
		this.language = language;
		this.errors = [];
		this.fields = {};
		this.form = this._formByNameOrNode(formNameOrNode) || {};
		this.messages = {};
		this.handlers = {};

		for (var i = 0, fieldLength = fields.length; i < fieldLength; i++) {
			var field = fields[i];

			// If passed in incorrectly, we need to skip the field.
			if ((!field.name && !field.names) || !field.rules) {
				continue;
			}

			/*
			 * Build the master fields array that has all the information needed to validate
			 */

			if (field.names) {
				for (var j = 0; j < field.names.length; j++) {
					this._addField(field, field.names[j]);
				}
			} else {
				this._addField(field, field.name);
			}
		}

		/*
		 * Attach an event callback for the form submission
		 */

		var _onsubmit = this.form.onsubmit;

		this.form.onsubmit = (function(that) {
			return function(evt) {
				try {
					return that._validateForm(evt) && (_onsubmit === undefined || _onsubmit());
				} catch(e) {
				}
			};
		})(this);
	}, attributeValue = function(element, attributeName) {
		var i;

		if ((element.length > 0) && (element[0].type === 'radio')) {
			for ( i = 0; i < element.length; i++) {
				if (element[i].checked) {
					return element[i][attributeName];
				}
			}

			return;
		}

		return element[attributeName];
	};

	/*
	 * @public
	 * Sets a custom message for one of the rules
	 */

	FormValidator.prototype.setMessage = function(rule, message) {
		this.messages[rule] = message;

		// return this for chaining
		return this;
	};

	/*
	 * @public
	 * Registers a callback for a custom rule (i.e. callback_username_check)
	 */

	FormValidator.prototype.registerCallback = function(name, handler) {
		if (name && typeof name === 'string' && handler && typeof handler === 'function') {
			this.handlers[name] = handler;
		}

		// return this for chaining
		return this;
	};

	/*
	 * @private
	 * Determines if a form dom node was passed in or just a string representing the form name
	 */

	FormValidator.prototype._formByNameOrNode = function(formNameOrNode) {
		return ( typeof formNameOrNode === 'object') ? formNameOrNode : document.forms[formNameOrNode];
	};

	/*
	 * @private
	 * Adds a file to the master fields array
	 */

	FormValidator.prototype._addField = function(field, nameValue) {
		this.fields[nameValue] = {
			name : nameValue,
			display : field.display || nameValue,
			rules : field.rules,
			id : null,
			type : null,
			value : null,
			checked : null
		};
	};

	/*
	 * @private
	 * Runs the validation when the form is submitted.
	 */

	FormValidator.prototype._validateForm = function(evt) {
		this.errors = [];

		for (var key in this.fields) {
			if (this.fields.hasOwnProperty(key)) {
				var field = this.fields[key] || {}, element = this.form[field.name];

				if (element && element !== undefined) {
					field.id = attributeValue(element, 'id');
					field.type = (element.length > 0) ? element[0].type : element.type;
					field.value = attributeValue(element, 'value');
					field.checked = attributeValue(element, 'checked');

					/*
					 * Run through the rules for each field.
					 */
					this._validateField(field);
				}
			}
		}

		if ( typeof this.callback === 'function') {
			this.callback(this.errors, evt);
		}

		if (this.errors.length > 0) {
			if (evt && evt.preventDefault) {
				evt.preventDefault();
			} else if (event) {
				// IE uses the global event variable
				event.returnValue = false;
			}
		}

		return true;
	};

	/*
	 * @private
	 * Looks at the fields value and evaluates it against the given rules
	 */

	FormValidator.prototype._validateField = function(field) {
		var rules = field.rules.split('|');

		/*
		 * If the value is null and not required, we don't need to run through validation, unless the rule is a callback, but then only if the value is not null
		 */

		if ((field.rules.indexOf('required') === -1 && (!field.value || field.value === '' || field.value === undefined)) && (field.rules.indexOf('callback_') === -1 || field.value === null)) {
			return;
		}

		/*
		 * Run through the rules and execute the validation methods as needed
		 */

		for (var i = 0, ruleLength = rules.length; i < ruleLength; i++) {
			var method = rules[i], param = null, failed = false, parts = ruleRegex.exec(method);

			/*
			 * If the rule has a parameter (i.e. matches[param]) split it out
			 */

			if (parts) {
				method = parts[1];
				param = parts[2];
			}

			/*
			 * If the hook is defined, run it to find any validation errors
			 */

			if ( typeof this._hooks[method] === 'function') {
				if (!this._hooks[method].apply(this, [field, param])) {
					failed = true;
				}
			} else if (method.substring(0, 9) === 'callback_') {
				// Custom method. Execute the handler if it was registered
				method = method.substring(9, method.length);

				if ( typeof this.handlers[method] === 'function') {
					if (this.handlers[method].apply(this, [field.value, param]) === false) {
						failed = true;
					}
				}
			}

			/*
			 * If the hook failed, add a message to the errors array
			 */

			if (failed) {
				// Make sure we have a message for this rule
				//

				var source = this.messages[method] || defaults[this.language][method], message = 'An error has occurred with the ' + field.display + ' field.';

				if (source) {
					message = source.replace('%s', field.display);

					if (param) {
						message = message.replace('%s', (this.fields[param]) ? this.fields[param].display : param);
					}
				}

				this.errors.push({
					id : field.id,
					name : field.name,
					message : message,
					rule : method
				});

				// Break out so as to not spam with validation errors (i.e. required and valid_email)
				break;
			}
		}
	};

	/*
	 * @private
	 * Object containing all of the validation hooks
	 */

	FormValidator.prototype._hooks = {
		required : function(field) {
			var value = field.value;

			if ((field.type === 'checkbox') || (field.type === 'radio')) {
				return (field.checked === true);
			}

			return (value !== null && value !== '');
		},

		"default" : function(field, defaultName) {
			return field.value !== defaultName;
		},

		matches : function(field, matchName) {
			var el = this.form[matchName];

			if (el) {
				return field.value === el.value;
			}

			return false;
		},

		valid_email : function(field) {
			return emailRegex.test(field.value);
		},

		valid_emails : function(field) {
			var result = field.value.split(",");

			for (var i = 0; i < result.length; i++) {
				if (!emailRegex.test(result[i])) {
					return false;
				}
			}

			return true;
		},

		min_length : function(field, length) {
			if (!numericRegex.test(length)) {
				return false;
			}

			return (field.value.length >= parseInt(length, 10));
		},

		max_length : function(field, length) {
			if (!numericRegex.test(length)) {
				return false;
			}

			return (field.value.length <= parseInt(length, 10));
		},

		exact_length : function(field, length) {
			if (!numericRegex.test(length)) {
				return false;
			}

			return (field.value.length === parseInt(length, 10));
		},

		greater_than : function(field, param) {
			if (!decimalRegex.test(field.value)) {
				return false;
			}

			return (parseFloat(field.value) > parseFloat(param));
		},

		less_than : function(field, param) {
			if (!decimalRegex.test(field.value)) {
				return false;
			}

			return (parseFloat(field.value) < parseFloat(param));
		},

		alpha : function(field) {
			return (alphaRegex.test(field.value));
		},

		alpha_numeric : function(field) {
			return (alphaNumericRegex.test(field.value));
		},

		alpha_dash : function(field) {
			return (alphaDashRegex.test(field.value));
		},

		numeric : function(field) {
			return (numericRegex.test(field.value));
		},

		integer : function(field) {
			return (integerRegex.test(field.value));
		},

		decimal : function(field) {
			return (decimalRegex.test(field.value));
		},

		is_natural : function(field) {
			return (naturalRegex.test(field.value));
		},

		is_natural_no_zero : function(field) {
			return (naturalNoZeroRegex.test(field.value));
		},

		valid_ip : function(field) {
			return (ipRegex.test(field.value));
		},

		valid_base64 : function(field) {
			return (base64Regex.test(field.value));
		},

		valid_url : function(field) {
			return (urlRegex.test(field.value));
		},

		valid_credit_card : function(field) {
			// Luhn Check Code from https://gist.github.com/4075533
			// accept only digits, dashes or spaces
			if (!numericDashRegex.test(field.value))
				return false;

			// The Luhn Algorithm. It's so pretty.
			var nCheck = 0, nDigit = 0, bEven = false;
			var strippedField = field.value.replace(/\D/g, "");

			for (var n = strippedField.length - 1; n >= 0; n--) {
				var cDigit = strippedField.charAt(n);
				nDigit = parseInt(cDigit, 10);
				if (bEven) {
					if ((nDigit *= 2) > 9)
						nDigit -= 9;
				}

				nCheck += nDigit;
				bEven = !bEven;
			}

			return (nCheck % 10) === 0;
		},

		is_file_type : function(field, type) {
			if (field.type !== 'file') {
				return true;
			}

			var ext = field.value.substr((field.value.lastIndexOf('.') + 1)), typeArray = type.split(','), inArray = false, i = 0, len = typeArray.length;

			for (i; i < len; i++) {
				if (ext == typeArray[i])
					inArray = true;
			}

			return inArray;
		}
	};

	window.FormValidator = FormValidator;

})(window, document); 